package io.alexkozlov.jpointfedagent;

import io.alexkozlov.jpointfedagent.model.crd.ServiceExport;
import io.alexkozlov.jpointfedagent.model.crd.ServiceImport;
import io.alexkozlov.jpointfedagent.operator.K8SOperator;
import io.fabric8.istio.api.networking.v1alpha3.*;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.StatusDetails;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

@Service
public class IstioController {

    private static final Logger LOGGER = LoggerFactory.getLogger(IstioController.class);

    private boolean createServiceIstioResources(ServiceImport serviceImport, ServiceExport serviceExport) {
        boolean b1 = createSIVirtualService(serviceImport, serviceExport);
        boolean b2 = createSIGateway(serviceImport, serviceExport);
        boolean b3 = createSIServiceEntry1(serviceImport, serviceExport);
        boolean b4 = createSIServiceEntry2(serviceImport, serviceExport);
        return b1 && b2 && b3 && b4;
    }

    private boolean createSIVirtualService(ServiceImport serviceImport, ServiceExport serviceExport) {
        VirtualService virtualService = new VirtualService();

        ObjectMeta metadata = new ObjectMeta();
        metadata.setName(String.format("f-e-%s-vs", serviceImport.getMetadata().getName()));
        metadata.setNamespace(serviceImport.getMetadata().getNamespace());

        virtualService.setMetadata(metadata);

        VirtualServiceSpec spec = new VirtualServiceSpec();
        spec.setHosts(Collections.singletonList(String.format("%s.clusterset.local", serviceImport.getMetadata().getName())));
        spec.setGateways(Collections.singletonList(String.format("f-%s", serviceImport.getMetadata().getName())));
        spec.setExportTo(Collections.singletonList("."));

        HTTPRoute route = new HTTPRoute();

        HTTPMatchRequest match = new HTTPMatchRequest();

        match.setGateways(Collections.singletonList(String.format("f-%s", serviceImport.getMetadata().getName())));
        match.setPort(Integer.parseInt(serviceImport.getSpec().port));

        route.setMatch(Collections.singletonList(match));

        HTTPRewrite rewrite = new HTTPRewrite();
        rewrite.setAuthority(String.format("ingressgateway-%s.%s", serviceExport.getSpec().namespace, serviceExport.getSpec().ingressHost));
        route.setRewrite(rewrite);

        HTTPRouteDestination destination = new HTTPRouteDestination();
        PortSelector portSelector = new PortSelector();
        portSelector.setNumber(80);

        Destination d = new Destination();
        d.setHost(String.format("ingressgateway-%s.%s", serviceExport.getSpec().namespace, serviceExport.getSpec().ingressHost));
        d.setPort(portSelector);
        destination.setDestination(d);

        Headers h = new Headers();
        HashMap<String, String> map = new HashMap<>();
        map.put("x-fed-origin-uri", String.format("/federation/%s/%s", serviceExport.getSpec().name, serviceExport.getSpec().port));

        HeadersHeaderOperations operations = new HeadersHeaderOperations();
        operations.setAdd(map);

        h.setRequest(operations);
        destination.setHeaders(h);
        destination.setWeight(100);

        route.setRoute(Collections.singletonList(destination));

        spec.setHttp(Collections.singletonList(route));

        virtualService.setSpec(spec);

        var client = getClient();
        LOGGER.info("SI VirtualService trying create");
        LOGGER.info(virtualService.toString());
        client.resources(VirtualService.class).inNamespace(serviceExport.getSpec().namespace).resource(virtualService).createOrReplace();;

        return true;
    }

    private boolean createSIGateway(ServiceImport serviceImport, ServiceExport serviceExport) {
        Gateway gateway = new Gateway();

        ObjectMeta metadata = new ObjectMeta();
        metadata.setName(String.format("f-%s", serviceImport.getMetadata().getName()));
        metadata.setNamespace(serviceImport.getMetadata().getNamespace());

        gateway.setMetadata(metadata);

        GatewaySpec spec = new GatewaySpec();
        Server server = new Server();
        server.setHosts(Collections.singletonList(String.format("%s.clusterset.local", serviceImport.getMetadata().getName())));
        Port port = new Port();
        port.setNumber(Integer.parseInt(serviceImport.getSpec().port));
        port.setName("http-" + serviceImport.getSpec().port);
        port.setProtocol("HTTP");
        server.setPort(port);

        HashMap<String, String> map = new HashMap<>();
        map.put("app", "egress");
        spec.setSelector(map);

        spec.setServers(Collections.singletonList(server));
        gateway.setSpec(spec);

        var client = getClient();
        LOGGER.info("SI Gateway trying create");
        LOGGER.info(gateway.toString());
        client.resources(Gateway.class).inNamespace(serviceExport.getSpec().namespace).resource(gateway).createOrReplace();;

        return true;
    }

    private boolean createSIServiceEntry1(ServiceImport serviceImport, ServiceExport serviceExport) {
        ServiceEntry serviceEntry = new ServiceEntry();

        ObjectMeta metadata = new ObjectMeta();
        metadata.setName(String.format("f-e-%s-%s-se", serviceImport.getMetadata().getName(), serviceImport.getSpec().port));
        metadata.setNamespace(serviceImport.getMetadata().getNamespace());

        serviceEntry.setMetadata(metadata);

        ServiceEntrySpec spec = new ServiceEntrySpec();
        spec.setHosts(Collections.singletonList(String.format("%s.clusterset.local", serviceImport.getMetadata().getName())));

        Port port = new Port();
        port.setNumber(Integer.parseInt(serviceImport.getSpec().port));
        port.setName("http");
        port.setProtocol("TCP");

        spec.setPorts(Collections.singletonList(port));
        spec.setLocation(ServiceEntryLocation.MESH_INTERNAL);
        spec.setResolution(ServiceEntryResolution.DNS);
        spec.setExportTo(Collections.singletonList("."));

        WorkloadEntrySpec workloadEntrySpec = new WorkloadEntrySpec();
        workloadEntrySpec.setAddress(String.format("federation-egressgateway.%s.svc.cluster.local", serviceImport.getMetadata().getNamespace()));
        workloadEntrySpec.setLocality(serviceExport.getSpec().clusterName);
        HashMap<String, Long> map = new HashMap<>();
        map.put("http", Long.valueOf(serviceImport.getSpec().port));
        workloadEntrySpec.setPorts(map);

        spec.setEndpoints(Collections.singletonList(workloadEntrySpec));
        serviceEntry.setSpec(spec);

        var client = getClient();
        LOGGER.info("SI ServiceEntry1 trying create");
        LOGGER.info(serviceEntry.toString());
        client.resources(ServiceEntry.class).inNamespace(serviceExport.getSpec().namespace).resource(serviceEntry).createOrReplace();;

        return true;
    }

    private boolean createSIServiceEntry2(ServiceImport serviceImport, ServiceExport serviceExport) {
        ServiceEntry serviceEntry = new ServiceEntry();

        ObjectMeta metadata = new ObjectMeta();
        metadata.setName(String.format("f-e-%s-%s-%s-se",
                serviceImport.getMetadata().getName(), serviceImport.getMetadata().getNamespace(),
                serviceImport.getSpec().name));
        metadata.setNamespace(serviceImport.getMetadata().getNamespace());

        serviceEntry.setMetadata(metadata);

        ServiceEntrySpec spec = new ServiceEntrySpec();
        spec.setHosts(Collections.singletonList(
                String.format("ingressgateway-%s.%s",
                        serviceExport.getSpec().namespace,
                        serviceExport.getSpec().ingressHost)));

        Port port = new Port();
        port.setNumber(80);
        port.setName("http-80");
        port.setProtocol("TCP");

        spec.setPorts(Collections.singletonList(port));
        spec.setLocation(ServiceEntryLocation.MESH_INTERNAL);
        spec.setResolution(ServiceEntryResolution.DNS);
        spec.setExportTo(Collections.singletonList("."));

        serviceEntry.setSpec(spec);

        var client = getClient();
        LOGGER.info("SI ServiceEntry2 trying create");
        LOGGER.info(serviceEntry.toString());
        client.resources(ServiceEntry.class).inNamespace(serviceExport.getSpec().namespace).resource(serviceEntry).createOrReplace();;

        return true;
    }

    public void createServiceImport(ServiceImport serviceImport, ArrayList<ServiceExport> serviceExports) {
        for (ServiceExport serviceExport : serviceExports) {
            createServiceIstioResources(serviceImport, serviceExport);
        }
    }

    public void createServiceExport(ServiceExport serviceExport) {
        VirtualService virtualService = new VirtualService();

        ObjectMeta metadata = new ObjectMeta();
        metadata.setName(String.format("f-i-%s-vs", serviceExport.getMetadata().getName()));
        metadata.setNamespace(serviceExport.getSpec().namespace);

        virtualService.setMetadata(metadata);

        VirtualServiceSpec spec = new VirtualServiceSpec();
        spec.setHosts(Collections.singletonList(String.format("ingressgateway-%s.%s", serviceExport.getSpec().namespace, serviceExport.getSpec().ingressHost)));
        spec.setGateways(Collections.singletonList("f-i-gw"));
        spec.setExportTo(Collections.singletonList("."));

        HTTPRoute route = new HTTPRoute();
        HTTPMatchRequest match = new HTTPMatchRequest();
        StringMatchBuilder stringMatchBuilder = new StringMatchBuilder();

        stringMatchBuilder.withNewStringMatchExactType(String.format("/federation/%s/%s", serviceExport.getSpec().name, serviceExport.getSpec().port));
        StringMatch exact = stringMatchBuilder.build();

        HashMap<String, StringMatch> headers = new HashMap<>();
        headers.put("x-fed-origin-uri", exact);

        match.setHeaders(headers);
        route.setMatch(Collections.singletonList(match));

        HTTPRouteDestination destination = new HTTPRouteDestination();
        PortSelector portSelector = new PortSelector();
        portSelector.setNumber(Integer.parseInt(serviceExport.getSpec().port));

        Destination d = new Destination();
        d.setHost(String.format("%s.%s.svc.cluster.local", serviceExport.getSpec().name, serviceExport.getSpec().namespace));
        d.setPort(portSelector);

        destination.setDestination(d);
        Headers h = new Headers();
        HeadersHeaderOperations operations = new HeadersHeaderOperations();

        operations.setRemove(Collections.singletonList("x-fed-origin-uri"));
        h.setRequest(operations);
        destination.setHeaders(h);

        route.setRoute(Collections.singletonList(destination));
        spec.setHttp(Collections.singletonList(route));
        virtualService.setSpec(spec);

        Gateway gateway = new Gateway();

        ObjectMeta metadataGW = new ObjectMeta();
        metadataGW.setName("f-i-gw");
        metadataGW.setNamespace(serviceExport.getSpec().namespace);

        gateway.setMetadata(metadataGW);
        GatewaySpec specGW = new GatewaySpec();
        specGW.setSelector(Collections.singletonMap("app", "ingress"));

        Server server = new Server();
        server.setHosts(Collections.singletonList(String.format("ingressgateway-%s.%s", serviceExport.getSpec().namespace, serviceExport.getSpec().ingressHost)));
        Port port = new Port();
        port.setName("http-8080");
        port.setNumber(8080);
        port.setProtocol("HTTP");
        server.setPort(port);

        specGW.setServers(Collections.singletonList(server));
        gateway.setSpec(specGW);

        var client = getClient();
        LOGGER.info("SE VirtualService trying create");
        LOGGER.info(virtualService.toString());
        client.resources(VirtualService.class).inNamespace(serviceExport.getSpec().namespace).resource(virtualService).createOrReplace();

        LOGGER.info("SE Gateway trying create");
        LOGGER.info(gateway.toString());
        client.resources(Gateway.class).inNamespace(serviceExport.getSpec().namespace).resource(gateway).createOrReplace();;
    }

    public void deleteServiceImport(ServiceImport serviceImport) {
        var nameVS = String.format("f-i-%s-vs", serviceImport.getMetadata().getName());
        var client = getClient();
        var d = client.resources(VirtualService.class).inNamespace(serviceImport.getMetadata().getNamespace()).withName(nameVS).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SI VirtualService deleted");
            LOGGER.info(vs.toString());
        }

        var nameSE1 = String.format("f-e-%s-%s-se", serviceImport.getMetadata().getName(), serviceImport.getSpec().port);
        d = client.resources(ServiceEntry.class).inNamespace(serviceImport.getMetadata().getNamespace()).withName(nameSE1).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SI ServiceEntry1 deleted");
            LOGGER.info(vs.toString());
        }

        var nameSE2 = String.format("f-e-%s-%s-%s-se",
                serviceImport.getMetadata().getName(), serviceImport.getMetadata().getNamespace(),
                serviceImport.getSpec().name);
        d = client.resources(ServiceEntry.class).inNamespace(serviceImport.getMetadata().getNamespace()).withName(nameSE2).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SI ServiceEntry2 deleted");
            LOGGER.info(vs.toString());
        }

        var nameGW = String.format("f-%s", serviceImport.getMetadata().getName());
        d = client.resources(Gateway.class).inNamespace(serviceImport.getMetadata().getNamespace()).withName(nameGW).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SI Gateway deleted");
            LOGGER.info(vs.toString());
        }
    }

    public void deleteServiceExport(ServiceExport serviceExport) {
        var name = String.format("f-i-%s-vs", serviceExport.getMetadata().getName());
        var client = getClient();
        var d = client.resources(VirtualService.class).inNamespace(serviceExport.getSpec().namespace).withName(name).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SE VirtualService deleted");
            LOGGER.info(vs.toString());
        }

        var nameGW = "f-i-gw";
        d = client.resources(Gateway.class).inNamespace(serviceExport.getSpec().namespace).withName(nameGW).delete();
        for (StatusDetails vs : d) {
            LOGGER.info("SE Gateway deleted");
            LOGGER.info(vs.toString());
        }
    }

    private KubernetesClient getClient() {
        KubernetesClient client = new DefaultKubernetesClient();
        if (System.getenv("DEBUG")!= null) {
            LOGGER.info("DEBUG MODE");
            String text;
            try {
                text = Files.readString(Paths.get(System.getenv("KUBECONFIG")));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            client = new DefaultKubernetesClient(Config.fromKubeconfig(text));
        }
        return client;
    }
}
